import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.regex.Pattern;

public class Client implements Runnable{
	
	/** Socket that the client is connected via. */
	private Socket socket;
	
	/** The server this client belongs to. */
	private Server server;
	
	/** Reads input from the socket. */
	private BufferedReader in;
	
	/** Prints output too the socket. */
	private PrintWriter out;
	
	/** Thread that listens for messages from the client. */
	private Thread listener = new Thread(this);
	
	/** The listeners that will be notified of a new message. */
	private ArrayList<ClientListener> listeners = new ArrayList<ClientListener>();
	
	/**
	 * Creates a new client connection for the given socket and begins listening for messages.
	 * @param socket The socket the client is connected via.
	 * @throws IOException If there was a problem setting up the IOStreams.
	 */
	public Client(Server server, Socket socket) throws IOException {
		if(server == null) throw new NullPointerException("Socket is null");
		if(socket == null) throw new NullPointerException("Socket is null");
		this.server = server;
		this.socket = socket;
		
		in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
		
		listener.start();
	}
	
	/**
	 * Returns the status of the clients connection. This is not guaranteed to return false
	 * if the client has unexpectedly disconnected.
	 * @return True if the client is connected, False if not.
	 */
	public boolean isConnected() { return socket != null; }
	
	/**
	 * Sends the given string to the client followed by a newline character.
	 * @param msg The message to send.
	 */
	public void sendMsg(String msg) {
		if(!isConnected()) throw new IllegalStateException("Client has disconnected.");
		
		out.println(msg);
		out.flush();
	}
	
	/**
	 * Sends a message to the client with the given pattern. The given arguments are inserted into the
	 * pattern which is then sent as a string to the client.
	 * @param p The pattern of the message to send.
	 * @param args The arguments to send in the message.
	 */
	public void sendMsg(Pattern p, Object... args) {
		String msgFormat = p.pattern();
		
		//TODO: Should verify that length of args is equal to number of capture groups in p.
		
		// Replace all the capturing groups with a %s format character
		msgFormat = msgFormat.replaceAll("\\(.*?\\)", "%s");
		
		// Format the string and insert the arguments.
		String msg = String.format(msgFormat, args);
		
		sendMsg(msg);
	}
	
	/**
	 * Adds the give message listener to the list of listeners to be notified of new messages
	 * Received by this client.
	 * @param l The listener to add.
	 */
	public void addMessageListener(ClientListener l) {
		listeners.add(l);
	}
	
	/**
	 * Disconnects the client by first sending a TERMINATION message then  closing the underlying
	 * IOStreams.
	 */
	public void disconnect() {
		if(socket != null) {
			// Send BYE message
			sendMsg(Messages.TERMINATION);
			out.flush();
		}
		try {
			close();
		}
		catch(IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Closes the underlying socket and IOStreams for the client.
	 * @throws IOException If there was a problem closing the streams.
	 */
	private void close() throws IOException {
		if(socket != null) {
			in.close();
			out.close();
			socket.close();
			
			socket = null;
			
			server.clientDisconnected(this);
		}
		
	}
	
	/**
	 * Returns the network address of the client.
	 * @return The network address of the client.
	 */
	public InetAddress getInetAddress() { return socket == null ? null:socket.getInetAddress(); }
	
	/**
	 * Listen thread the listens for messages from the client
	 */
	@Override
	public void run() {
		while(isConnected()) {
			try {
				// Read message
				String line = in.readLine();
				
				if(line != null) {
					// Notify the listeners
					listeners.forEach(l -> {
						l.messageRecieved(this, line);
					});
				}
			}
			catch(SocketException e) {
				try {
					close();		// Try closing
				} catch (IOException e1) {
					socket = null;	// All else fails set socket to null and it will be "disconnected".
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}
